/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows.Controls;
using Xceed.Silverlight.Data;
using System.Diagnostics;

namespace Xceed.Silverlight.ListBox
{
  public sealed class SelectedItemsWrapper : IList, INotifyCollectionChanged, INotifyPropertyChanged
  {
    #region Constructor
    public SelectedItemsWrapper( ListBox listBox )
    {
      if( listBox == null )
        throw new NullReferenceException( "ListBox should not be null." );

      m_listBox = listBox;
      m_selectedItems = new List<object>( 128 );
      m_selectedItemsLookup = new HashSet<object>();
    }
    #endregion

    internal event EventHandler Completed;

    internal bool UpdateSelectedItems()
    {
      int oldSelectedItemsCount = m_selectedItems.Count;

      m_selectedItems.Clear();
      m_selectedItemsLookup.Clear();

      IList<SelectionRange> selectedRanges = m_listBox.SelectedRanges;

      int selectedRangesCount = selectedRanges.Count;

      // No selectedRanges
      if( selectedRangesCount == 0 )
      {
        // If selectedItems contained elements, notify
        if( oldSelectedItemsCount > 0 )
        {
          this.RaiseChangedEventsAndUpdateSelectionProperties(
             new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
        }

        return true;
      }

      // We return as if we succeeded the update
      // in async mode
      if( m_listBox.IsAsyncDataSource )
        return true;

      DataSourceWrapper dataSourceWrapper = m_listBox.DataSourceWrapper;
      DataRangeContext dataRangeContext = dataSourceWrapper.GetCurrentDataRangeContext();

      bool isSelectAll = false;

      for( int i = 0; i < selectedRangesCount; i++ )
      {
        SelectionRange selectionRange = selectedRanges[ i ];

        if( selectionRange.SelectFromStart && selectionRange.SelectToEnd )
        {
          isSelectAll = true;
          break;
        }
      }

      if( isSelectAll )
      {
        if( m_listBox.IsUnbound )
        {
          m_selectedItems.AddRange( m_listBox.Items.Cast<object>() );

          m_selectedItemsLookup = new HashSet<object>( m_selectedItems );

          this.RaiseChangedEventsAndUpdateSelectionProperties(
            new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );

          return true;
        }
        else
        {
          Debug.Assert( dataSourceWrapper != null );
          Debug.Assert( ( dataSourceWrapper.Provider as ListDataSourceProvider ) != null );

          SelectedItemsAsyncResult selectedItemsResult = new SelectedItemsAsyncResult( selectedRanges,
            this.OnSelectedItemsAsyncResultCompleted,
            null,
            dataSourceWrapper );

          selectedItemsResult.Launch();

          return true;
        }
      }
      else
      {
        SelectedItemsAsyncResult selectedItemsResult = new SelectedItemsAsyncResult( selectedRanges,
          this.OnSelectedItemsAsyncResultCompleted,
          null,
          dataSourceWrapper );

        selectedItemsResult.Launch();

        return false;
      }
    }

    private void OnSelectedItemsAsyncResultCompleted( IAsyncResult asyncResult )
    {
      if( asyncResult == null )
        throw new ArgumentNullException( "asyncResult" );

      SelectedItemsAsyncResult selectedItemsAsyncResult = asyncResult as SelectedItemsAsyncResult;

      if( selectedItemsAsyncResult == null )
        throw new ArgumentException( "The specified asyncResult must be a SelectedItemAsyncResult.", "asyncResult" );

      m_selectedItems.AddRange( selectedItemsAsyncResult.SelectedItems );
      m_selectedItemsLookup = new HashSet<object>( m_selectedItems );

      this.RaiseCompleted();

      this.RaiseChangedEventsAndUpdateSelectionProperties(
        new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
    }

    private void RaiseCompleted()
    {
      if( this.Completed == null )
        return;

      this.Completed( this, EventArgs.Empty );
    }

    private void RaiseChangedEventsAndUpdateSelectionProperties( NotifyCollectionChangedEventArgs args )
    {
      this.RaisePropertyChanged( "Count" );
      this.RaisePropertyChanged( "Item[]" );
      this.RaiseCollectionChanged( args );
      this.UpdateSelectionPropertiesOnContainers();
    }

    private void UpdateSelectionPropertiesOnContainers()
    {
      if( m_listBox == null )
        return;

      PanelBase panelBase = m_listBox.Panel;

      if( panelBase == null )
        return;

      panelBase.UpdateSelectionPropertiesOnContainers();
    }

    private HashSet<object> m_selectedItemsLookup;
    private List<object> m_selectedItems;
    private ListBox m_listBox;

    #region INotifyCollectionChanged Members

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    private void RaiseCollectionChanged( NotifyCollectionChangedEventArgs args )
    {
      if( this.CollectionChanged == null )
        return;

      this.CollectionChanged( this, args );
    }

    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged( string propertyName )
    {
      if( this.PropertyChanged == null )
        return;

      if( propertyName == null )
        propertyName = string.Empty;

      this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
    }

    #endregion

    #region IList Members

    public int Add( object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotSupportedException( "SelectedItems is read-only for asynchronous data sources." );

      if( m_listBox.SelectionMode == SelectionMode.Single )
        throw new InvalidOperationException( "Can only change SelectedItems collection in multiple selection modes. Use SelectedItem in single selection modes." );

      if( value == null )
        throw new ArgumentNullException( "value cannot be null." );

      if( m_selectedItemsLookup.Contains( value ) )
        throw new InvalidOperationException( "Item is already in SelectedItems" );

      int addedIndex = m_selectedItems.Count;
      this.Insert( addedIndex, value );

      return addedIndex;
    }

    public void Clear()
    {
      int oldSelectedItemsCount = m_selectedItems.Count;

      m_selectedItems.Clear();
      m_selectedItemsLookup.Clear();
      m_listBox.SelectedRanges.Clear();

      if( oldSelectedItemsCount > 0 )
      {
        this.RaiseChangedEventsAndUpdateSelectionProperties(
          new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
      }
    }

    public bool Contains( object value )
    {
      if( value == null )
        throw new ArgumentNullException( "value" );

      return m_selectedItemsLookup.Contains( value );
    }

    public int IndexOf( object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

      if( value == null )
        throw new ArgumentNullException( "value" );

      return m_selectedItems.IndexOf( value );
    }

    public void Insert( int index, object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

      if( ( index < 0 ) || ( index > m_selectedItems.Count ) )
        throw new ArgumentOutOfRangeException( "index is out of range." );

      if( value == null )
        throw new ArgumentNullException( "value cannot be null." );

      if( m_selectedItemsLookup.Contains( value ) )
        throw new InvalidOperationException( "Item is already in SelectedItems." );

      using( m_listBox.DeferUpdateSelection() )
      {

        DataSourceWrapper dataSourceWrapper = m_listBox.DataSourceWrapper;

        DataPath dataPath = dataSourceWrapper.CreateDataPathForObject( value );

        SelectionRange newRange = SelectionManager.SelectionRangeHelper( dataSourceWrapper,
          dataPath,
          true,
          dataPath,
          true );

        m_listBox.SelectedRanges.Add( newRange );
        m_selectedItems.Insert( index, value );
        m_selectedItemsLookup.Add( value );

        this.RaiseChangedEventsAndUpdateSelectionProperties(
          new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, value, index ) );
      }
    }

    public bool IsFixedSize
    {
      get
      {
        return m_listBox.IsAsyncDataSource;
      }
    }

    public bool IsReadOnly
    {
      get
      {
        return m_listBox.IsAsyncDataSource;
      }
    }

    public void Remove( object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

      if( value == null )
        throw new ArgumentNullException( "value cannot be null" );

      int removedIndex = m_selectedItems.IndexOf( value );
      this.RemoveAt( removedIndex );
    }

    public void RemoveAt( int index )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous datas sources." );

      if( ( index < 0 ) || ( index >= m_selectedItems.Count ) )
        throw new ArgumentOutOfRangeException( "index" );

      using( m_listBox.DeferUpdateSelection() )
      {
        object removedItem = m_selectedItems[ index ];
        m_selectedItems.RemoveAt( index );
        m_selectedItemsLookup.Remove( removedItem );

        DataSourceWrapper dataSourceWrapper = m_listBox.DataSourceWrapper;
        IList<SelectionRange> selectedRanges = m_listBox.SelectedRanges;

        DataPath dataPath = dataSourceWrapper.CreateDataPathForObject( removedItem );

        SelectionRange removedRange = SelectionHelper.FindSelectionRangeForPath( dataSourceWrapper,
          dataPath,
          selectedRanges );

        selectedRanges.Remove( removedRange );

        if( !removedRange.IsSingleItem )
        {
          IEnumerable<SelectionRange> splitRanges = SelectionManager.SplitSelectedRange( dataSourceWrapper,
            removedRange,
            dataPath,
            dataPath );

          foreach( SelectionRange splitRange in splitRanges )
            selectedRanges.Add( splitRange );
        }

        this.RaiseChangedEventsAndUpdateSelectionProperties(
          new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, removedItem, index ) );
      }
    }

    public object this[ int index ]
    {
      get
      {
        // Do not throw for invalid index
        if( ( index < 0 ) || ( index >= m_selectedItems.Count ) )
          return null;

        return m_selectedItems[ index ];
      }
      set
      {
        if( m_listBox.IsAsyncDataSource )
          throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

        if( ( index < 0 ) || ( index >= m_selectedItems.Count ) )
          throw new ArgumentOutOfRangeException( "index" );

        object oldValue = m_selectedItems[ index ];

        if( oldValue != value )
        {
          if( oldValue != null )
          {
            this.Remove( oldValue );
          }

          if( value != null )
          {
            this.Insert( index, value );
          }

          this.RaiseChangedEventsAndUpdateSelectionProperties(
            new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Replace, value, oldValue, index ) );
        }
      }
    }

    #endregion

    #region ICollection Members

    public void CopyTo( Array array, int index )
    {
      ( ( ICollection )m_selectedItems ).CopyTo( array, index );
    }

    public int Count
    {
      get
      {
        return m_selectedItems.Count;
      }
    }

    public bool IsSynchronized
    {
      get
      {
        return ( ( ICollection )m_selectedItems ).IsSynchronized;
      }
    }

    public object SyncRoot
    {
      get
      {
        return ( ( ICollection )m_selectedItems ).SyncRoot;
      }
    }

    #endregion

    #region IEnumerable Members

    public IEnumerator GetEnumerator()
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotSupportedException( "Use ListBox.[Begin|End]GetSelectedItems." );

      return m_selectedItems.GetEnumerator();
    }

    #endregion
  }
}
